from datetime import datetime, timedelta
from decimal import Decimal
from threading import Thread

from django.core.mail import EmailMultiAlternatives
from django.db.models import Q
from rest_framework.exceptions import ValidationError

from apps.booking.helpers.create_appointment import save_client_services
from apps.booking.signals import client_check_in
from apps.client.models import Client
from apps.core.utils import date_time as datetime_helper
from django.conf import settings

from apps.billing.models import ClientInvoice, InvoiceServices
from apps.booking.models import Attendance, Appointment, Booking, AppointmentClientServices
from apps.booking.telehealth import create_meeting, create_zoom_meeting
from apps.booking.types import AttendanceStatusChoices, AppointmentStatusChoices
from apps.tenant.models import ProviderLocation, ProviderServices
from apps.users.models import Provider


def appointment_json(start_date, instance, i=None):
    return Appointment(
        start_date=start_date,
        is_recurrent_of=instance,
        start_time=instance.start_time,
        end_time=instance.end_time,
        booking=instance.booking,
        provider_location=instance.provider_location,
        weekly_repeat=instance.weekly_repeat,
        zoom=instance.zoom,
        google=instance.google,
        telehealth=instance.telehealth,
        select_dates=instance.select_dates,
        selected_dates=instance.selected_dates,
        telehealth_zoom_id=None,
        telehealth_google_id=None,
        number_of_weeks=((instance.number_of_weeks - i) if (i and instance.number_of_weeks > 0) and instance.weekly_repeat is True else 0)
    )


def repeating_telehealths_meetings_link(provider, appointment):
    if appointment.telehealth is True:
        if appointment.zoom:
            appointment.telehealth_zoom_id = create_zoom_meeting(
                provider,
                appointment.start_date,
                appointment.start_time,
                appointment.end_time)
        if appointment.google:
            appointment.telehealth_google_id = create_meeting(
                provider,
                appointment.start_date,
                appointment.start_time,
                appointment.end_time)


def update_appointment(request, instance, validated_data):
    data = request.data

    data=validated_data
    start_date = validated_data.get('start_data', instance.start_date)
    start_time = validated_data.get('start_time', instance.start_time)
    end_time = validated_data.get('end_time', instance.end_time)

    if 'start_date' in validated_data:
        start_date = validated_data.get('start_date')
        validated_data['start_date'] = datetime.fromtimestamp(start_date)

    if 'checked_in' in validated_data:
        checked_in = validated_data.get('checked_in', False)
        if checked_in:

            for client in instance.clients.all():
                Attendance.objects.create(appointment=instance,
                                          status=AttendanceStatusChoices.SHOW.value,
                                          client=client)
            user = request.user
            if user.is_provider:
                provider = user.provider
                client_check_in.send(sender=instance, instance=instance, provider=provider)
            elif user.is_client:
                provider_id = user.client.user_provider
                provider = Provider.objects.filter(id=provider_id).first()
                if provider:
                    client_check_in.send(sender=instance, instance=instance, provider=provider, client=user)

    if 'telehealth' in validated_data:
        telehealth = validated_data.get('telehealth', instance.telehealth)
        if telehealth is False and instance.telehealth is True:
            instance.telehealth_google_id = None
            instance.telehealth_zoom_id = None

            instance.save()

        if telehealth is True:
            google = validated_data.get('google', False)
            zoom = validated_data.get('zoom', False)
            if google:
                instance.telehealth_google_id = create_meeting(request.user,
                                                               start_date,
                                                               start_time,
                                                               end_time)
            if zoom:
                instance.telehealth_zoom_id = create_zoom_meeting(request.user,
                                                                  start_date, start_time, end_time)
            if instance.google is True and google is False:
                instance.google = False
                instance.telehealth_google_id = None
            if instance.zoom is True and zoom is False:
                instance.zoom = False
                instance.teleheatlh_zoom_id = None
            instance.save()

    # If Client removes those checks from edit resulting Repeating bookings after the instance booking will be
    # deleted
    is_recurrent_of = instance.is_recurrent_of if instance.is_recurrent_of else instance
    if 'select_dates' in validated_data or 'weekly_repeat' in validated_data:
        select_dates = validated_data.get('select_dates')
        weekly_repeat = validated_data.get('weekly_repeat')

        if select_dates is False and instance.select_dates is True:
            Appointment.objects.filter(is_recurrent_of=is_recurrent_of,
                                       start_date__gt=instance.start_date).delete()
            instance.selected_dates = None
        if weekly_repeat is False and instance.weekly_repeat is True:
            Appointment.objects.filter(is_recurrent_of=is_recurrent_of,
                                       start_date__gt=instance.start_date).delete()

    # Handling repeating functionality for editing appointments
    if 'selected_dates' in validated_data:
        selected_dates = validated_data['selected_dates']
        handle_repeating_select_dates(request.user, instance, selected_dates)

    if 'number_of_weeks' in validated_data or 'weekly_repeat_gap' in validated_data:
        number_of_weeks = validated_data.get('number_of_weeks', instance.number_of_weeks)
        weekly_repeat_gap = validated_data.get('weekly_repeat_gap', instance.weekly_repeat_gap)
        handle_repeating_weekly_repeat(number_of_weeks, instance, weekly_repeat_gap)

    instance = handle_flags(instance, validated_data)

    if 'provider_location' in data:
        provider_location = data.get('provider_location')
        data['provider_location'] = ProviderLocation.objects.filter(id=provider_location).first()
    # instance = super().update(instance, validated_data)
    services = None
    if 'appointment_services' in data:
        services = data.pop('appointment_services')
    clients = None
    if 'clients' in data:
        clients = data.pop('clients')
    for field_name, field_value in data.items():
        if hasattr(Appointment, field_name):
            setattr(instance, field_name, field_value)

    instance.save()
    if 'status' in validated_data:
        status = validated_data.get('status')
        save_or_update_attendance(instance, status)

    if clients:
        instance.clients.set(clients)

    if isinstance(clients, list) and len(clients) <= 0:
        instance.clients.clear()
    # Re-creating the appointment service list
    if services:
        AppointmentClientServices.objects.filter(appointment=instance).delete()
        save_client_services(instance, services)
        if instance.appointment_invoice.exists():
            appointment_invoices = instance.appointment_invoice.all()
            InvoiceServices.objects.filter(invoice__in=instance.appointment_invoice.all()).delete()

            for appointment_invoice in appointment_invoices:
                for service_id, service in enumerate(services):
                    total_fees = Decimal(0.0)
                    appointment_services = service['services']
                    for appointment_service in appointment_services:
                        pro_services = ProviderServices.objects.filter(id=appointment_service['service']).first()
                        client = Client.objects.filter(id=service['id']).first()
                        invoice_service = {
                            'invoice': appointment_invoice,
                            'date': datetime.timestamp(instance.start_date),
                            # 'service': appointment_service['service'],
                            'fees': appointment_service['fees']
                        }
                        if pro_services:
                            invoice_service['service'] = pro_services
                        else:
                            raise ValidationError({'msg': 'Service does not exist'})

                        if client:
                            invoice_service['client'] = client
                        else:
                            raise ValidationError({'msg': 'Client does not exist'})


                        if 'date' in invoice_service and invoice_service['date'] is not None:
                            invoice_service['date'] = datetime.fromtimestamp(invoice_service.get('date'))

                        invoice_service = InvoiceServices.objects.create(**invoice_service)

                        total_fees += Decimal(invoice_service.fees)

                    ClientInvoice.objects.filter(id=appointment_invoice.id,
                                                 client=appointment_invoice.client).update(fees=total_fees)
                    services.pop(service_id)

    if isinstance(services, list) and len(services) <= 0:
        AppointmentClientServices.objects.filter(appointment=instance).delete()

    return instance


def handle_repeating_weekly_repeat(number_of_weeks, instance, weekly_repeat_gap):
    appointments = []
    # if instance.is_recurrent_of:
    parent_appointment = instance.is_recurrent_of if instance.is_recurrent_of else instance
    existing_appointments = Appointment.objects.filter(is_recurrent_of=parent_appointment,
                                                       start_date__gt=instance.start_date)
    existing_appointments.delete()
    if instance.weekly_repeat is True and int(number_of_weeks) > 0:
        next_start_date = instance.start_date
        start_time = instance.start_time
        end_time = instance.end_time
        number_of_appointments = int(number_of_weeks) * int(weekly_repeat_gap)
        repeated_dates = []
        j = 1
        clients = instance.clients.all()
        next_start_date = next_start_date + timedelta(days=1)
        week_appointment_count = 0
        i = 0
        while i < number_of_appointments:
            provider_flag = validate_provider_in_booking_weekly_repeat(
                                                                       start_time, end_time,
                                                                       next_start_date.date())
            client_flag = validate_client_in_booking_weekly_repeat(clients, start_time, end_time,
                                                                   next_start_date.date())
            if provider_flag is False or client_flag is False:
                next_start_date += timedelta(days=1)
                continue
            week_start = next_start_date - timedelta(days=next_start_date.weekday())
            week_end = week_start + timedelta(days=6)
            if week_appointment_count >= instance.weekly_repeat_gap:
                next_start_date = week_end + timedelta(days=1)
                week_appointment_count = 0
                continue
            week_appointment_count += 1
            i += 1
            repeated_dates.append(next_start_date)
            next_start_date += timedelta(days=1)
        for date in repeated_dates:
            start_date = date
            appointment = appointment_json(start_date, instance, i)
            repeating_telehealths_meetings_link(appointment)

            appointments.append(appointment)
    if len(appointments) > 0:
        appointments_created = Appointment.objects.bulk_create(appointments)

        appointment_services = instance.appointment_services.all()
        appointment_services = list(appointment_services)

        for appointment in appointments_created:
            for client in instance.clients.all():
                appointment.clients.add(client)
                appointment.save()

            for service in appointment_services:
                AppointmentClientServices.objects.create(appointment=appointment, client=service.client,
                                                         service=service.service, fees=service.fees)
    return instance




def validate_client_in_booking_weekly_repeat(client, start_time, end_time, date):
    range_time = (start_time, end_time)
    query = Appointment.objects.filter(clients__in=client)
    query = query.filter(start_date__date=date)
    for q in query:
        if (q.start_time < start_time < q.end_time) or \
                (q.start_time < end_time < q.end_time):
            return False
    query = query.filter(
        Q(start_time__range=range_time) |
        Q(end_time__range=range_time))
    query = query.exclude(Q(end_time=start_time) |
                          Q(start_time=end_time))

    if query.count() > 0:
        return False
    return True


def validate_provider_in_booking_weekly_repeat(start_time, end_time, date):
    range_time = (start_time, end_time)
    query = Booking.objects.all()
    query = query.filter(start_date__date=date)
    for q in query:
        if (q.start_time < start_time < q.end_time) or \
                (q.start_time < end_time < q.end_time):
            return False
    query = query.filter(
        Q(start_time__range=range_time) |
        Q(end_time__range=range_time))
    query = query.exclude(Q(end_time=start_time) |
                          Q(start_time=end_time))

    if query.count() > 0:
        return False

    query = Appointment.objects.all()
    query = query.filter(start_date__date=date)
    for q in query:
        if (q.start_time < start_time < q.end_time) or \
                (q.start_time < end_time < q.end_time):
            return False
    query = query.filter(
        Q(start_time__range=range_time) |
        Q(end_time__range=range_time))
    query = query.exclude(Q(end_time=start_time) |
                          Q(start_time=end_time))

    if query.count() > 0:
        return False
    return True

def handle_flags(instance: Appointment, validated_data):
    access_granted = validated_data.get('access_granted', False)
    if access_granted:
        # schedule_revoke_access_granted(instance)
        instance.access_granted_at = datetime_helper.now()
        return instance

    checked_in = validated_data.get('checked_in', False)
    if checked_in:
        # check_in_client_appointment.send(
        #     sender=AppointmentRequestSerializer, instance=instance)
        # schedule_revoke_checked_in(instance)
        instance.checked_in_at = datetime_helper.now()
        return instance
    return instance


def save_or_update_attendance(instance, status):
    attendances = Attendance.objects.filter(appointment=instance)
    attendance_status = None
    if status == AppointmentStatusChoices.COMPLETE.value:
        attendance_status = AttendanceStatusChoices.SHOW.value
    elif status == AppointmentStatusChoices.CANCELLED.value:
        attendance_status = AttendanceStatusChoices.CANCELLED.value
    elif status == AppointmentStatusChoices.LATE_CANCELLED.value:
        attendance_status = AttendanceStatusChoices.LATE_CANCELLED.value
    elif status == AppointmentStatusChoices.NO_SHOW.value:
        attendance_status = AttendanceStatusChoices.NO_SHOW.value
    if attendance_status:
        if attendances.exists():
            for attendance in attendances:
                attendance.status = attendance_status
                attendance.save()
        else:
            for client in instance.clients.all():
                Attendance.objects.create(appointment=instance, client=client, status=attendance_status)



def diff(list1, list2):
    missing_elements = [x for x in list1 if x not in list2]
    return missing_elements


def handle_repeating_select_dates(provider, instance, selected_dates=None):
    appointments = []
    if instance.booking is None:
        try:
            if instance.select_dates:
                parent_appointment = instance.is_recurrent_of if instance.is_recurrent_of else instance
                appointment_selected_dates = [date.date() for date in parent_appointment.selected_dates]
                appointment_selected_dates.append(parent_appointment.start_date.date())
                repeating_dates = [date.date() for date in selected_dates]
                new_added_dates = diff(repeating_dates, appointment_selected_dates)
                if len(new_added_dates) > 0:
                    for date in new_added_dates:
                        appointment = appointment_json(date, parent_appointment)
                        repeating_telehealths_meetings_link(provider, appointment)
                        appointments.append(appointment)
                    if len(appointments) > 0:
                        appointments_created = Appointment.objects.bulk_create(appointments)

                        appointment_services = instance.appointment_services.all()
                        appointment_services = list(appointment_services)

                        for appointment in appointments_created:
                            for client in instance.clients.all():
                                appointment.clients.add(client)
                                appointment.save()

                            for service in appointment_services:
                                AppointmentClientServices.objects.create(appointment=appointment,
                                                                         client=service.client,
                                                                         service=service.service, fees=service.fees)

        except Exception as e:
            print(e)
            from_email = settings.CONTACT_EMAIL
            subject = 'Sorry for the inconvenience. Your repeating appointments could not be created'

            msg = EmailMultiAlternatives(subject=subject, from_email=from_email, to=[])
            try:
                msg.send()
            except Exception as ex:
                print(ex)

    return instance